﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Security.Authentication;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace K9Nano.Admin.Weixin.OAuth
{
    internal class WeixinOAuthHandler : OAuthHandler<WeixinOAuthOptions>
    {
        private const string ClientId = "clientId";
        public const string WeixinSettingKey = "weixinSetting";

        protected readonly ICachedPropertiesSecureDataFormat CachedPropertiesSecureDataFormat;
        protected readonly IHttpClientFactory HttpClientFactory;
        protected readonly IWeixinAppSettingManager WeixinAppSettingManager;

        public WeixinOAuthHandler(
            IOptionsMonitor<WeixinOAuthOptions> options,
            ILoggerFactory loggerFactory,
            UrlEncoder encoder,
            ISystemClock clock,
            ICachedPropertiesSecureDataFormat cachedPropertiesSecureDataFormat,
            IHttpClientFactory httpClientFactory,
            IWeixinAppSettingManager weixinAppSettingManager)
            : base(options, loggerFactory, encoder, clock)
        {
            CachedPropertiesSecureDataFormat = cachedPropertiesSecureDataFormat;
            HttpClientFactory = httpClientFactory;
            WeixinAppSettingManager = weixinAppSettingManager;
        }

        /// <summary>
        /// Called after options/events have been initialized for the handler to finish initializing itself.
        /// </summary>
        /// <returns>A task</returns>
        protected override async Task InitializeHandlerAsync()
        {
            await base.InitializeHandlerAsync();
            Options.StateDataFormat ??= CachedPropertiesSecureDataFormat;
            Options.Backchannel ??= HttpClientFactory.CreateClient();
        }

        protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
        {
            /*
             * currentUri=/Weixin/Authorize?clientId=&returnUrl=
             *
             */

            var currentUri = CurrentUri;
            Logger.RedirectUrl(currentUri);
            var queryStrings = QueryHelpers.ParseQuery(new Uri(currentUri).Query);
            StringValues clientIdValue = StringValues.Empty;
            if (queryStrings.ContainsKey(ClientId))
            {
                clientIdValue = queryStrings[ClientId];
            }
            if (StringValues.IsNullOrEmpty(clientIdValue))
            {
                throw new AuthenticationException("ClientId missing");
            }

            // 跳转到微信授权页面, see: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

            var isMicroMessenger = Options.IsWeChatBrowser(Request);

            string scope, endpoint;

            if (isMicroMessenger)
            {
                scope = "snsapi_userinfo";
                endpoint = Options.AuthorizationEndpoint;
            }
            else
            {
                scope = "snsapi_userinfo,snsapi_login";
                endpoint = Options.QrCodeAuthorizationEndpoint;
            }

            var weixinSetting = WeixinAppSettingManager.Get(clientIdValue.ToString());
            if (weixinSetting == null)
            {
                throw new AuthenticationException("Weixin app settings missing");
            }
            properties.RedirectUri = currentUri;
            properties.SetParameter(WeixinSettingKey, weixinSetting);

            var state = Options.StateDataFormat.Protect(properties);

            var parameters = new Dictionary<string, string>()
            {
                { "appid", weixinSetting.AppId },
                { "redirect_uri", redirectUri },
                { "response_type", "code" }
            };

            var ret = QueryHelpers.AddQueryString(endpoint, parameters);
            ret += $"&scope={scope}&state={state}";

            Logger.AuthorizeUrl(ret);

            return ret;
        }

        protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
        {
            Logger.AuthorizeCallbackUrl(Request.GetDisplayUrl());

            var query = Request.Query;

            var code = query["code"];
            var state = query["state"];

            if (StringValues.IsNullOrEmpty(code) || StringValues.IsNullOrEmpty(state))
            {
                return HandleRequestResult.Fail("The state or code was missing or invalid.");
            }

            var properties = Options.StateDataFormat.Unprotect(state);
            if (properties == null)
            {
                return HandleRequestResult.Fail("The oauth state was missing or invalid.");
            }

            // OAuth2 10.12 CSRF
            //if (!ValidateCorrelationId(properties))
            //{
            //    return HandleRequestResult.Fail("Correlation failed.");
            //}

            if (StringValues.IsNullOrEmpty(code))
            {
                return HandleRequestResult.Fail("Code was missing.");
            }

            var tokens = await ExchangeCodeAsync(new OAuthCodeExchangeContext(properties, code, "/"));

            if (tokens.Error != null)
            {
                return HandleRequestResult.Fail(tokens.Error);
            }

            if (string.IsNullOrEmpty(tokens.AccessToken))
            {
                return HandleRequestResult.Fail("Failed to retrieve access token.");
            }

            var identity = new ClaimsIdentity(ClaimsIssuer);

            if (Options.SaveTokens)
            {
                var authTokens = new List<AuthenticationToken>();

                authTokens.Add(new AuthenticationToken { Name = "access_token", Value = tokens.AccessToken });

                if (!string.IsNullOrEmpty(tokens.RefreshToken))
                {
                    authTokens.Add(new AuthenticationToken { Name = "refresh_token", Value = tokens.RefreshToken });
                }

                if (!string.IsNullOrEmpty(tokens.TokenType)) //微信就没有这个
                {
                    authTokens.Add(new AuthenticationToken { Name = "token_type", Value = tokens.TokenType });
                }

                if (!string.IsNullOrEmpty(tokens.ExpiresIn))
                {
                    if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
                    {
                        // https://www.w3.org/TR/xmlschema-2/#dateTime
                        // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
                        var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
                        authTokens.Add(new AuthenticationToken
                        {
                            Name = "expires_at",
                            Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                        });
                    }
                }

                properties.StoreTokens(authTokens);
            }

            var ticket = await CreateTicketAsync(identity, properties, tokens);
            if (ticket != null)
            {
                return HandleRequestResult.Success(ticket);
            }

            return HandleRequestResult.Fail("Failed to retrieve user information from remote server.");
        }

        protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context)
        {
            var weixinSetting = context.Properties.GetParameter<WeixinAppSettingInfo>(WeixinSettingKey);
            var parameters = new Dictionary<string, string>
            {
                {  "appid", weixinSetting.AppId },
                {  "secret", weixinSetting.AppSecret },
                {  "code", context.Code},
                {  "grant_type", "authorization_code" }
            };

            var endpoint = QueryHelpers.AddQueryString(Options.TokenEndpoint, parameters);

            var response = await Backchannel.GetAsync(endpoint, Context.RequestAborted);
            if (response.IsSuccessStatusCode)
            {
                var body = await response.Content.ReadAsStringAsync();
                if (body.StartsWith("{\"errcode\""))
                {
                    return OAuthTokenResponse.Failed(new AuthenticationException(body));
                }
                var payload = JsonDocument.Parse(body);
                return OAuthTokenResponse.Success(payload);
            }

            return OAuthTokenResponse.Failed(new HttpRequestException("获取微信AccessToken出错: " + response.StatusCode));
        }

        protected override async Task<AuthenticationTicket> CreateTicketAsync(
            ClaimsIdentity identity,
            AuthenticationProperties properties,
            OAuthTokenResponse tokens)
        {
            var openId = tokens.Response.RootElement.GetString("openid");

            // 微信获取用户信息是需要开通权限的，没有开通权限的只能用openId来标示用户
            var parameters = new Dictionary<string, string>
                {
                    {  "openid", openId},
                    {  "access_token", tokens.AccessToken },
                    {  "lang", "zh-CN" }
                };
            var userInfoEndpoint = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, parameters);
            var userInfoResponse = await Backchannel.GetAsync(userInfoEndpoint, Context.RequestAborted);
            if (!userInfoResponse.IsSuccessStatusCode)
            {
                throw new HttpRequestException($"未能获取到微信用户个人信息(返回状态码:{userInfoResponse.StatusCode})，请检查access_token是正确。");
            }

            var body = await userInfoResponse.Content.ReadAsStringAsync();
            if (body.StartsWith("{\"errcode\""))
            {
                Logger.UserInfoResponseFailed(body);
                throw new AuthenticationException("获取微信用户信息失败");
            }
            Logger.UserInfoResponse(body);
            var userInfo = JsonDocument.Parse(body);

            var weixinSetting = properties.GetParameter<WeixinAppSettingInfo>(WeixinSettingKey);
            identity.AddClaim(new Claim("urn:wechat:appid", weixinSetting.AppId));

            var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity),
                properties,
                Context,
                Scheme,
                Options,
                Backchannel,
                tokens,
                userInfo.RootElement);
            context.RunClaimActions();
            await Events.CreatingTicket(context);

            return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
        }
    }
}
